Skip to main content

Nonprofit verification: API

Use the Goodstack API to build a nonprofit verification flow inside your product. Your interface collects the application, your backend submits it to Goodstack, webhook events prompt updates, and reconciliation reads confirm the current outcome.

This guide covers one server-side integration from organisation search through entitlement. If you want Goodstack to host the applicant interface, use the hosted verification flow.

Before you start​

Before you build the full flow, confirm that your publishable key can call the sandbox List Available Countries API. You will also need:

  • a publishable key for organisation-discovery requests;
  • a secret key for submissions, documents, reads, and webhook subscriptions;
  • the configurationId that Goodstack assigned to your program.

Retrieve your keys from the Keys page in the Partner dashboard. If the page or sandbox credentials are not available to your account, ask your Goodstack implementation contact to enable access.

Pass either API key as the raw value of the Authorization header. Do not add Bearer. Keep your secret key on your backend and never expose it in browser code, mobile apps, logs, or source control.

Confirm your program configuration

Before you build the applicant flow, confirm these details with your Goodstack implementation contact:

  • which organisation types and countries your program accepts;
  • whether applicants can enter an organisation that is not in search results;
  • which applicant fields and supporting documents your checks require;
  • whether an outcome can change after the first decision;
  • the failure and reapplication experience you want applicants to see.

These choices affect request validation and client behavior. The configuration endpoints return basic configuration data, but do not expose every check or document requirement.

Use https://sandbox-api.goodstack.io while you build. The examples below use <publishable-key> and <secret-key> placeholders for your sandbox credentials.

API map​

These are the Goodstack requests used by the core flow and its manual-entry branch:

PurposeRequestCredentialRuns from
Populate the country selectorGET /v1/countriesPublishable key in a browser; secret key on your backendBrowser or your backend
Find a registered nonprofitGET /v1/organisationsPublishable key in a browser; secret key on your backendBrowser or your backend
List registries for manual entryGET /v1/registriesPublishable key in a browser; secret key on your backendBrowser or your backend
Create a verificationPOST /v1/validation-submissionsSecret keyYour backend
Upload one supporting file when expectedPOST /v1/validation-submission-documentsSecret keyYour backend
Register result deliveryPOST /v1/webhook-subscriptionsSecret keyYour backend
Reconcile the current outcomeGET /v1/validation-submissions/{submissionId}Secret keyYour backend

The three discovery requests can run directly from the browser because they accept a publishable key. You can instead proxy them through your backend—for example, to centralise caching, filtering, or rate limiting. In that design, the browser calls your backend and only your backend calls Goodstack. A backend proxy can use either key, but must never return or forward a secret key to the browser.

Backend proxy rate limits

Publishable-key requests are rate limited per source IP, while secret-key requests are rate limited per partner. When using a backend proxy, publishable-key traffic shares the proxy's outbound-IP allowance. Confirm current limits with Goodstack when planning higher-volume integrations.

How the flow works​

1. Find the organisation​

Ask first for the country where the organisation is registered. Goodstack uses three-letter ISO country codes such as GBR, FRA, and USA. You can populate the country selector with the List Available Countries API.

Search by country and organisation name with the Search Organisations API. This read can run from your front end with a publishable key:

GET https://sandbox-api.goodstack.io/v1/organisations?countryCode=GBR&type[]=nonprofit&query=community%20foundation
Authorization: <publishable-key>
Accept: application/json

You can also search with the exact registryId parameter. Present enough context for the applicant to distinguish similarly named organisations.

A result is shaped like this:

{
"data": [
{
"id": "organisation_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"name": "Example Community Foundation",
"displayName": "Example Community Foundation",
"description": "A community foundation supporting local charities and projects.",
"countryCode": "GBR",
"types": ["nonprofit"],
"logo": "https://assets.example.org/example-community-foundation.png",
"registry": "registry_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"registryId": "1234567",
"registryDetails": {
"id": "registry_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"name": "Charity Commission",
"englishName": "Charity Commission",
"code": "CHC"
},
"website": "https://foundation.example.org",
"address": "10 Example Street, London, SW1A 1AA",
"addressLine1": "10 Example Street",
"addressLine2": null,
"city": "London",
"state": "England",
"postcode": "SW1A 1AA"
}
],
"totalResults": 1,
"pageNumber": 1,
"pageSize": 25,
"exhaustiveTotalResults": true,
"object": "organisation"
}

Some organisation fields can be null or absent. Use the registry number, location, and website to help applicants distinguish similarly named results; use the selected organisation's id as the stable API identifier.

When the applicant selects a result, store its id. Send that value as organisationId when you create the submission.

When the organisation is not found​

Most nonprofit programs allow applicants to enter an organisation that is not in the search results. When your program configuration allows it, offer a manual entry path and replace the search results with a form that collects the organisation's details. Goodstack uses this information to perform a Validation Request as part of the validation submission and assess the organisation's identity and nonprofit status. If the path is not enabled for your program, keep the applicant in your approved search or support flow instead of sending a manual-entry payload.

Give the applicant a way to return to search, and explain that continuing starts an organisation review that normally requires official supporting evidence.

An illustrative front-end structure could look like this; adapt the wording to your approved applicant experience:

Carry the country selected for search into this form, while allowing the applicant to correct it. Use the List Registries API to populate the registry selector for that country:

GET https://sandbox-api.goodstack.io/v1/registries?countryCode=GBR
Authorization: <publishable-key>
Accept: application/json

Submit the selected registry's name as registryName, not its Goodstack id. Submit the organisation's own record number in that registry as registryId. If the correct registry is not listed, registryName also accepts the registry's official name as free text.

For example:

Organisation jurisdictionregistryName exampleregistryId example
England and WalesCharity CommissionCharity number, such as 1234567
United StatesInternal Revenue ServiceEIN, such as 12-3456789

Use the exact name returned by the Registries API when one is available; the examples show how the registry name and the organisation's identifier map to separate submission fields.

For the nonprofit-only configuration covered by this guide, collect these organisation details:

FieldFront-end guidance
organisationNameAsk for the organisation's public name.
registryNameOffer the official registries returned for the selected country, plus free text.
registryIdLabel this for the selected registry, such as “charity number” or “EIN.”
websiteAsk for the organisation's official website.
countryCodeCarry forward the selected three-letter ISO country code.

These requirements are scoped to the nonprofit-only manual path.

When the applicant continues, collect their details and preferred language, then send everything to your backend using the manual-entry submission payload. Store the returned validation-submission ID before uploading the expected organisation evidence in step 4. Keep the benefit gated while Goodstack reviews the organisation and the other configured checks.

Programs that accept social-impact organisations

This guide's examples target a nonprofit-only configuration. If your program also accepts social_impact organisations, confirm the search filters and manual-entry requirements for that configuration before reusing these examples.

2. Create the validation submission​

Your backend creates a submission with its secret key. Always pass the configurationId assigned to the program; do not rely on an account default. You can list the IDs available to your account with the Retrieve Partner Configurations API.

The examples below assume that applicant/agent verification is enabled, so they include firstName, lastName, and email. Goodstack will confirm the fields required by your actual configuration.

Use this payload when the applicant selected a search result:

POST https://sandbox-api.goodstack.io/v1/validation-submissions
Authorization: <secret-key>
Idempotency-Key: cause_attempt_<your-id>_submission
Content-Type: application/json

{
"configurationId": "hostedconfiguration_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"organisationId": "organisation_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"firstName": "Alex",
"lastName": "Doe",
"email": "alex.doe@example.org",
"language": "en-GB",
"metadata": {
"applicationId": "application_12345"
}
}

metadata lets you attach up to 20 of your own string values to the submission. Keys can contain up to 40 characters and values up to 250 characters. Include a stable application or account ID so webhook events can be reconciled with your system without using applicant details as the lookup key.

Make retries safe

Create one verification-attempt record in your system before calling Goodstack. Use one stable Idempotency-Key for that logical create request. If the request times out, retry with the same key and the identical payload.

Use a different key for every other logical write, including each document upload. Never reuse a key with a changed payload or across two endpoints. Do not build around a fixed key-retention window.

Store the response​

Goodstack returns 200 OK with the current submission. Persist data.id before updating the applicant experience. The other fields and nested check objects depend on the configured checks:

{
"data": {
"id": "validationsubmission_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"status": "pending",
"organisationId": "organisation_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"validationRequestId": null,
"agentVerificationId": "agentverification_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"eligibilitySubscriptionId": "eligibilitysubscription_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"monitoringSubscriptionId": "monitoringsubscription_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"createdAt": "2026-08-07T14:30:00.000Z",
"metadata": {
"applicationId": "application_12345"
}
},
"object": "validation_submission"
}

Store:

  • data.id as the canonical validation-submission ID;
  • data.organisationId when present;
  • each non-null component ID that your configuration returns;
  • your own application ID and the idempotency key used for the create request.

A manual submission starts with organisationId: null and has a validationRequestId. If Goodstack validates the organisation, later reads and webhooks include its organisationId. Component IDs can be null when their checks are not configured, so your integration must not require every ID in the example.

3. Show the applicant the current state​

Drive the main applicant experience from the top-level submission status:

StatusClient action
pendingKeep the benefit gated. Show an in-review state or the configured next step.
succeededIf your approved program policy maps success to access, apply the benefit. Store any newly assigned organisationId.
failedKeep the benefit gated and show your approved failure or recovery experience.

Do not assume the create response will be pending; handle all three statuses. Checks run asynchronously and can take from a few seconds to as long as 72 hours. When the response is pending, release the request and use result delivery and reconciliation rather than keeping the applicant's browser request open.

Nested check statuses can explain progress, but they do not replace the top-level outcome. In particular, do not infer that the applicant needs association evidence from agentVerification.status. Follow the manual-entry evidence guidance in step 4, and use the requirements Goodstack confirmed for any additional document steps.

Dynamic outcomes

Some configurations can recalculate an outcome after the initial decision. If Goodstack confirms that dynamic outcomes are enabled for your program, process validation_submission.updated and apply your agreed entitlement or review policy to the latest retrieved status.

4. Upload supporting documents​

Plan to collect organisation evidence when an applicant uses manual entry. Almost all manual-entry configurations require it because the organisation was not selected from Goodstack search results. Only omit this step when Goodstack explicitly confirms that your configuration is one of the rare exceptions.

For an organisation selected from search results, request a document only when your configuration requires one. Goodstack may also require separate evidence of the applicant's association with the organisation.

Recommend evidence for a manually entered nonprofit​

For the typical manual-entry path, ask the applicant for the clearest current official evidence of the organisation's nonprofit or charitable status. Recommend one or more of these documents, starting with the strongest evidence available in the organisation's jurisdiction:

  • a registration, recognition, or good-standing certificate issued by a charity, nonprofit, association, foundation, or other competent regulator;
  • a current official registry extract, status confirmation, registration decision, or renewal notice;
  • an official-gazette notice or government registration receipt that establishes the organisation's legal existence;
  • a tax-authority determination or recognition letter that explicitly confirms charitable, nonprofit, or tax-exempt status;
  • a certificate of incorporation, formation, or registration that identifies the entity's nonprofit legal form.

If no single official document establishes both legal identity and nonprofit status, the applicant can also provide a constitution, articles of association, bylaws, trust deed, foundation charter, or similar governing document alongside the strongest official evidence available.

Prefer documents that clearly show the legal name, registration or charity number, issuing authority, jurisdiction, status, and any issue, renewal, or expiry date. These details should match the manual-entry payload. Do not use a donation receipt, bank statement, website screenshot, or marketing material as the primary proof of nonprofit status.

Upload nonprofit-status evidence with documentType=validation_request. Use documentType=agent_verification only when the document instead proves the applicant's association with the organisation.

Your backend sends the file to the Create Validation Submission Document API with its secret key:

curl --request POST \
--url 'https://sandbox-api.goodstack.io/v1/validation-submission-documents' \
--header 'Authorization: <secret-key>' \
--header 'Idempotency-Key: cause_attempt_<your-id>_document_1' \
--form 'validationSubmissionId=validationsubmission_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx' \
--form 'documentType=validation_request' \
--form 'file=@./registration-proof.pdf'

Let your HTTP client set the multipart Content-Type, including its boundary. Upload one file per request.

Choose documentType by what the evidence supports:

documentTypeUse it for
validation_requestEvidence about the organisation, such as registration documentation
agent_verificationEvidence about the applicant's association with the organisation

Send the value explicitly rather than relying on the default.

A successful upload returns:

{
"data": {
"id": "validationsubmissiondocument_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"validationSubmissionId": "validationsubmission_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"createdAt": "2026-08-07T14:35:00.000Z",
"url": "https://assets.example.org/document.pdf",
"type": "validation_request"
},
"object": "validation_submission_document"
}

The endpoint accepts JPEG, PNG, and PDF content up to 5 MB. Goodstack detects the file type from the file's bytes, not its extension or Content-Type label. A HEIC image renamed to .jpg, for example, is still rejected. Validate or convert files before upload, and do not log document contents or presigned document URLs.

After upload, store data.id with the verification attempt, keep the benefit gated, and show the applicant the in-review state. The upload response is not an outcome; wait for a webhook or the next scheduled reconciliation read.

5. Configure result delivery​

Complete result delivery before accepting applications. Keeping this setup next to event processing makes the full asynchronous result path easier to implement and test. Use the Create Webhook Subscription API from your backend with your secret key. The request uses the plural events property and requires an HTTPS URL:

POST https://sandbox-api.goodstack.io/v1/webhook-subscriptions
Authorization: <secret-key>
Content-Type: application/json

{
"events": [
"validation_submission.created",
"validation_submission.succeeded",
"validation_submission.failed",
"validation_submission.updated"
],
"url": "https://example.org/webhooks/goodstack"
}

The create response contains the secret used to verify this subscription's deliveries:

{
"data": {
"id": "webhooksubscription_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"events": [
"validation_submission.created",
"validation_submission.succeeded",
"validation_submission.failed",
"validation_submission.updated"
],
"url": "https://example.org/webhooks/goodstack",
"secret": "<webhook-subscription-secret>",
"createdAt": "2026-08-07T14:00:00.000Z",
"updatedAt": null,
"deletedAt": null
},
"object": "webhook_subscription"
}

Store data.id and data.secret in your backend's secret store as soon as you receive them. The subscription secret is separate from both API keys; do not expose it to applicants, commit it, or write it to logs. Subscribe to all four events so the same handler works if dynamic outcomes are enabled later.

6. Process and reconcile results​

The endpoint configured in step 5 receives these Validation Submission events:

  • validation_submission.created;
  • validation_submission.succeeded;
  • validation_submission.failed;
  • validation_submission.updated.

The updated event is used by configurations whose outcomes can change.

An event uses this envelope:

{
"object": "event",
"data": {
"id": "event_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"createdAt": "2026-08-07T15:00:00.000Z",
"eventType": "validation_submission.succeeded",
"eventData": {
"id": "validationsubmission_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"status": "succeeded",
"organisationId": "organisation_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"agentVerificationId": "agentverification_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"eligibilitySubscriptionId": "eligibilitysubscription_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"monitoringSubscriptionId": "monitoringsubscription_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"validationSubmissionHostedConfigurationId": "hostedconfiguration_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"metadata": {
"applicationId": "application_12345"
}
}
}
}

Treat delivery as an at-least-once signal:

  1. Verify Goodstack-Signature against the raw request body with the webhook subscription's secret, not an API key. Compute the hex-encoded HMAC-SHA256 over the unmodified body and compare the received and expected signatures in constant time.
  2. Deduplicate delivery on the event data.id.
  3. Persist the event and enqueue any slow work.
  4. Return a 2xx response after the event is safely stored.
  5. Retrieve the current submission before applying a state transition that must be authoritative.

Make the state or entitlement update idempotent by submission ID and the latest retrieved state, not only by event ID. Distinct events can describe the same effective outcome, and retries or out-of-order deliveries must not grant a benefit twice or overwrite a newer state.

Retrieve the latest submission​

Use webhooks as the prompt for state changes. Use the Retrieve Validation Submission API as the authoritative read for reconciliation, support tooling, or recovery after an uncertain delivery:

GET https://sandbox-api.goodstack.io/v1/validation-submissions/validationsubmission_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Authorization: <secret-key>
Accept: application/json

The response contains the latest submission and configured check results:

{
"data": {
"id": "validationsubmission_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"status": "succeeded",
"organisationId": "organisation_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"agentVerification": {
"id": "agentverification_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"status": "approved",
"rejectionReasonCode": null
},
"eligibility": {
"status": "live",
"results": {
"eligibilityStatus": "pass",
"confirmedActivitySubTags": [],
"rejectedActivitySubTags": []
}
},
"metadata": {
"applicationId": "application_12345"
}
},
"object": "validation_submission"
}

Nested check objects and result keys are configuration-dependent. Treat the read's top-level status—not a partial webhook snapshot or a nested check status—as the current verification outcome.

Do not assume that every non-terminal transition produces a webhook. Run a bounded reconciliation job that periodically retrieves submissions for which your system has not recorded a terminal outcome, and stops or escalates them according to the cadence and age limits agreed during onboarding. If dynamic outcomes are enabled, also follow the agreed resynchronization policy after a terminal outcome. Apply the same idempotent state-transition logic to webhook-triggered and scheduled reads.

7. Deliver the benefit and communicate the outcome​

After retrieving the latest submission, use its top-level status to complete the applicant journey. Keep the verification outcome separate from your own benefit-fulfilment state: Goodstack determines whether the verification succeeded, while your system determines whether and when the benefit has been delivered.

Complete the happy path​

When status is succeeded and your program policy maps success to the benefit:

  1. Persist the successful outcome and the latest organisationId against your application.
  2. Create or update one fulfilment record for the verification attempt.
  3. Grant the benefit, account access, discount, entitlement, or other program outcome exactly once.
  4. Record whether fulfilment is pending, complete, or needs operational attention.
  5. Communicate the result and the next step to the applicant.

Key fulfilment on your application or validation-submission ID so processing the same webhook again cannot grant the benefit twice. Acknowledge the webhook after storing the event; benefit fulfilment can run asynchronously through your normal job or queue infrastructure.

Tell the applicant what they need to know about both verification and fulfilment:

Fulfilment stateApplicant communication
CompleteConfirm verification, name the benefit now available, and explain how to access or use it.
In progressConfirm verification, explain that benefit delivery is underway, and give the next update or expected timing if known.
Needs operational helpKeep the verification marked successful, explain that benefit delivery is delayed, and provide the appropriate support path.

Use your own applicant-facing application reference where one is helpful. Do not include API keys, webhook details, raw check or screening results, uploaded-document links, or internal failure data in the confirmation. If the benefit has limits, an expiry, or another activation step, include that information so the applicant knows what to do next.

If dynamic outcomes are enabled, agree how a later status change affects an already delivered benefit and how you will communicate that change. Do not silently remove access without the review and communication policy agreed for the program.

When the submission status is failed​

A validation_submission.failed event is a verification outcome, not an API error. Persist the outcome against the verification attempt, keep the benefit gated, and show the failure or recovery experience agreed for your program.

A failed event can include a failureReasons array with structured context about the checks that produced the outcome. For a typical manual-entry failure, it can look like this:

{
"object": "event",
"data": {
"id": "event_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"createdAt": "2026-08-07T16:00:00.000Z",
"eventType": "validation_submission.failed",
"eventData": {
"id": "validationsubmission_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"status": "failed",
"organisationId": null,
"validationRequestId": "validationrequest_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"metadata": {
"applicationId": "application_12345"
},
"failureReasons": [
{
"check": "validation_request",
"reason": {
"rejectionReasonCode": "incorrect_documentation"
}
}
]
}
}
}

Use each check value to choose an internal handling path:

checkWhat it tells you
validation_requestGoodstack could not validate the manually entered organisation.
agent_verificationGoodstack could not verify the applicant's association with the organisation.
eligibilityThe organisation did not meet the configured eligibility criteria, or its eligibility could not be determined.
complianceThe organisation did not pass the configured compliance policy.
Other or absentKeep the benefit gated and route the attempt through your generic support or review path.

The shape of reason depends on the check. Validation Request and agent-verification failures can include a rejectionReasonCode; eligibility and compliance failures contain their relevant result data. Treat check, reason, and any nested values as extensible. Use them as machine-readable context for support, analytics, and recovery routing, not as applicant-facing copy. In particular, do not expose raw compliance screening results.

A Validation Request failure may be the only item because downstream checks cannot proceed until the organisation is validated. The array therefore explains the outcome; it is not necessarily a list of every check that would have failed.

Decide the recovery path​

Agree during onboarding which outcomes allow the applicant to correct information, replace a document, contact support, or start another attempt. Do not automatically create a new submission whenever a failed webhook arrives.

If your program permits reapplication, treat it as a new logical verification attempt with a new create-submission idempotency key, and retain the earlier submission ID for audit and support. If dynamic outcomes are enabled, a failed submission can change later; apply the agreed dynamic-outcome policy before starting a competing attempt.

Troubleshoot API request failures​

An unsuccessful API request is not a verification outcome. A request can fail before Goodstack accepts or completes it; a submission reaches failed only after Goodstack has created the verification and evaluated its configured checks. Do not show an applicant a verification-failure message solely because an API request failed.

SituationHandling
400 validation errorInspect the error response and correct the request. A changed write is a new logical request and needs a new idempotency key.
401 or 403Treat this as an integration configuration problem. Check the key, environment, and account scope before retrying.
404Check the endpoint, resource ID, configuration ID, and whether sandbox and production values were mixed.
429 or a 5xx responseRetry with backoff and honor Retry-After when present. For a write, keep the request body and idempotency key unchanged.
Timeout or lost responseThe result is uncertain. Retry the identical request with the same idempotency key, then reconcile before creating another verification attempt.

Keep the applicant's benefit gated while the request is unresolved. Record enough context to diagnose the request, but do not log API keys, uploaded documents, or other sensitive payload data.

Go live​

Before changing to https://api.goodstack.io:

  • switch the base URL and API keys together so sandbox and production credentials cannot mix;
  • confirm the production configurationId and document requirements;
  • verify and deduplicate webhooks in a staging-like environment;
  • test each public status and every configured document path;
  • test benefit fulfilment retries without granting the benefit twice;
  • approve applicant messages for success, delayed fulfilment, failure, and reapplication;
  • confirm how your product grants, withholds, or revisits the benefit;
  • ensure logs and analytics contain IDs, not secret keys or document contents.

Next steps​